const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');

/**
 * 数据库备份函数（使用 spawn + --defaults-file 安全导出）
 * 解决问题：
 *  - 密码含特殊字符导致命令解析失败
 *  - exec 对大数据量支持差（stdout 截断）
 *  - 导出不完整或为空
 *  - 缺少事务一致性保证
 */
const backupDatabase = () => {
    return new Promise((resolve, reject) => {
        // 1. 获取当前时间并格式化为文件名（避免 : . 等非法字符）
        const date = new Date();
        const formattedDate = date.toISOString().replace(/[:.]/g, '-').slice(0, -5); // 示例: 2025-04-05-14-30-00
        const backupFileName = `backup-${formattedDate}.sql`;
        const backupDir = path.join(__dirname, '../db/backups'); // 备份存储目录
        const backupFilePath = path.join(backupDir, backupFileName);

        // 2. 确保备份目录存在
        if (!fs.existsSync(backupDir)) {
            try {
                fs.mkdirSync(backupDir, { recursive: true });
                console.log(`📁 创建备份目录: ${backupDir}`);
            } catch (err) {
                return reject(new Error(`无法创建备份目录: ${err.message}`));
            }
        }

        // 3. 数据库配置（优先从环境变量读取）
        const dbUser = process.env.DB_USER || 'zjh';
        const dbPassword = process.env.DB_PASSWORD || 'xusir';
        const dbName = 'superBackend'; // 要备份的数据库名
        const dbHost = process.env.DB_HOST || 'localhost';
        const dbPort = process.env.DB_PORT || '3306';

        // 4. 创建临时 my.cnf 配置文件，避免密码暴露在命令行中
        const tmpConfigPath = path.join(backupDir, `.my-${formattedDate}.cnf`);
        const configContent = `
[client]
user=${dbUser}
password=${dbPassword}
host=${dbHost}
port=${dbPort}
protocol=tcp
`;
        try {
            fs.writeFileSync(tmpConfigPath, configContent);
            // 设置文件权限（仅当前用户可读写），提高安全性
            fs.chmodSync(tmpConfigPath, 0o600);
        } catch (err) {
            return reject(new Error(`无法创建临时配置文件: ${err.message}`));
        }

        // 5. 准备 mysqldump 命令参数数组（推荐方式，避免 shell 注入）
        const dumpArgs = [
            `--defaults-file=${tmpConfigPath}`,       // 使用安全配置文件
            '--single-transaction',                  // InnoDB 一致性快照，不锁表
            '--routines',                            // 包含存储过程和函数
            '--triggers',                            // 包含触发器
            '--hex-blob',                            // 安全导出 BLOB/BINARY 字段
            '--lock-tables=false',                   // 不锁定表（配合 single-transaction）
            '--set-gtid-purged=OFF',                 // 避免 GTID 相关错误（适用于大多数场景）
            '--comments',                            // 添加注释信息
            dbName                                   // 指定要备份的数据库
        ];

        // 6. 启动 mysqldump 子进程（使用 spawn 更适合大输出流）
        const mysqldump = spawn('mysqldump', dumpArgs);

        // 7. 创建写入流，将 mysqldump 输出写入文件
        const fileStream = fs.createWriteStream(backupFilePath);

        // 8. 捕获 stderr 输出（用于报错信息）
        let stderrOutput = '';
        mysqldump.stderr.on('data', (data) => {
            const chunk = data.toString();
            stderrOutput += chunk;
            // 可选：打印警告但不停止（如 "Using a password..." 提示是正常的）
            if (!chunk.includes('Using a password on the command line')) {
                console.warn('[mysqldump] ', chunk.trim());
            }
        });

        // 9. 将 stdout 数据通过管道写入文件
        mysqldump.stdout.pipe(fileStream);

        // 10. 监听文件写入完成事件
        fileStream.on('finish', () => {
            // 删除临时配置文件（无论成败都尝试删除）
            try {
                if (fs.existsSync(tmpConfigPath)) {
                    fs.unlinkSync(tmpConfigPath);
                }
            } catch (e) {
                console.warn(`⚠️ 无法删除临时配置文件: ${tmpConfigPath}`);
            }

            // 检查是否生成了空文件
            let stats;
            try {
                stats = fs.statSync(backupFilePath);
                if (stats.size === 0) {
                    return reject(new Error('备份文件已创建但内容为空，请检查数据库连接、权限或表是否存在'));
                }
            } catch (err) {
                return reject(new Error(`无法读取备份文件: ${err.message}`));
            }

            // 成功完成
            console.log(`✅ 数据库成功备份到: ${backupFilePath} (大小: ${stats.size} 字节)`);

            resolve({
                success: true,
                filePath: backupFilePath,
                fileName: backupFileName,
                timestamp: formattedDate,
                size: stats.size,
                message: '备份成功'
            });
        });

        // 11. 监听子进程异常或退出
        fileStream.on('error', (err) => {
            reject(new Error(`写入备份文件失败: ${err.message}`));
        });

        mysqldump.on('error', (err) => {
            // 清理临时文件
            try {
                if (fs.existsSync(tmpConfigPath)) fs.unlinkSync(tmpConfigPath);
                if (fs.existsSync(backupFilePath)) fs.unlinkSync(backupFilePath); // 删除残缺文件
            } catch (e) { }
            reject(new Error(`启动 mysqldump 失败: ${err.message}`));
        });

        mysqldump.on('close', (code) => {
            if (code !== 0 && !stderrOutput.includes('Using a password')) {
                // 如果 exit code 非零且有错误输出，则视为失败
                try {
                    fs.unlinkSync(backupFilePath); // 删除不完整的备份文件
                } catch (e) { }
                reject(new Error(`mysqldump 进程退出码 ${code}: ${stderrOutput || '未知错误'}`));
            }
            // 注意：'finish' 事件会接着触发，所以这里不 resolve
        });
    });
};

// ========================
// 可选：自动清理过期备份（保留最近 N 天）
// ========================
/**
 * 清理旧备份文件（基于文件名中的时间戳）
 * 支持格式：backup-YYYY-MM-DDTHH-mm-ss.sql
 * @param {number} days - 保留最近 N 天的备份
 */
const cleanupOldBackups = (days = 7) => {
  const backupDir = path.join(__dirname, '../db/backups');
  const now = new Date();
  const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1000); // 7天前

  try {
    const files = fs.readdirSync(backupDir);

    for (const filename of files) {
      const filePath = path.join(backupDir, filename);

      // 跳过非目标文件
      if (!filename.startsWith('backup-') || !filename.endsWith('.sql')) continue;

      // 使用正则提取：年-月-日 T 时-分-秒
      const match = filename.match(/backup-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})\.sql$/);
      if (!match) {
        console.warn(`⚠️ 无法匹配文件名格式: ${filename}`);
        continue;
      }

      const [, year, month, day, hour, minute, second] = match;

      // 构造标准 ISO 格式时间字符串（注意：使用 '-' 和 ':'）
      const isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`;
      const fileTime = new Date(isoString);

      // 验证时间是否有效
      if (isNaN(fileTime.getTime())) {
        console.warn(`⚠️ 无法解析时间: ${isoString} (文件: ${filename})`);
        continue;
      }

      // 判断是否过期
      if (fileTime < cutoff) {
        try {
          fs.unlinkSync(filePath);
          console.log(`🗑️ 已删除过期备份: ${filename} (${fileTime.toLocaleString()})`);
        } catch (err) {
          console.error(`❌ 删除文件失败 [${filename}]: ${err.message}`);
        }
      } else {
        console.log(`✅ 保留文件: ${filename} (${fileTime.toLocaleString()})`);
      }
    }
  } catch (err) {
    console.warn(`清理旧备份时出错: ${err.message}`);
  }
};

/**
 * 将整个项目备份到指定的驱动器（如 D:\、E:\）
 * @param {string} targetDrive - 目标盘符，例如 'D:' 或 'E:'
 * @param {Array<string>} excludeList - 用户自定义的额外排除项（文件或文件夹名）
 * @returns {Promise<Object>} 返回一个 Promise，成功时包含备份结果信息
 */
const backupProjectToDrive = (targetDrive = 'E:', excludeList = []) => {
  return new Promise((resolve, reject) => {
    // 1. 获取当前项目根目录的绝对路径（即这个脚本所在的目录）
    const projectRoot = path.resolve(__dirname, '../../');
    console.log(`项目根目录: ${projectRoot}`);
    
    // 2. 获取项目文件夹的名称，例如 'my-project'
    const projectName = path.basename(projectRoot);
    console.log(`项目名称: ${projectName}`);
    
    // 3. 获取当前时间并格式化为 YYYY-MM-DD，用于备份目录命名
    const now = new Date();
    const timestamp = now.toISOString().slice(0, 10); // 例如 '2025-09-10'
    // 4. 构造备份目录的完整路径，例如 D:\backups\my-project-2025-09-10
    const backupFolderName = `${projectName}-${timestamp}`;
    const targetDir = path.join(targetDrive, 'backups', backupFolderName);

    // 5. 定义默认要排除的文件/文件夹列表（避免备份不必要的大文件）
    const defaultExcludes = [
      'node_modules',     // Node.js 依赖包，体积大，可重新安装
      '.git',             // Git 版本控制历史，通常不需要备份
      'logs',             // 日志文件夹
      'tmp',              // 临时文件夹
    //   'temp',             // 另一个临时文件夹
    //   'dist',             // 构建后的前端资源（可选是否备份）
    //   '*.log',            // 所有 .log 文件
    //   'npm-debug.log*',   // npm 调试日志
    //   'yarn-debug.log*',  // yarn 调试日志
    //   'yarn-error.log*',  // yarn 错误日志
    ];

    // 6. 合并默认排除项和用户自定义排除项，并去重
    const finalExcludes = [...new Set([...defaultExcludes, ...excludeList])];

    // --- 验证和准备阶段 ---

    // 7. 检查目标盘符是否存在（例如 D:\）
    try {
      if (!fs.existsSync(targetDrive)) {
        return reject(new Error(`目标盘符不存在: ${targetDrive}`));
      }
    } catch (err) {
      return reject(new Error(`无法访问目标盘符: ${targetDrive} (${err.message})`));
    }

    // 8. 创建目标备份目录（如果不存在）
    try {
      // 先确保 D:\backups 目录存在
      if (!fs.existsSync(path.dirname(targetDir))) {
        fs.mkdirSync(path.dirname(targetDir), { recursive: true });
      }
      // 再创建具体的备份目录，例如 D:\backups\my-project-2025-09-10
      if (!fs.existsSync(targetDir)) {
        fs.mkdirSync(targetDir, { recursive: true });
      }
    } catch (err) {
      return reject(new Error(`无法创建目标备份目录: ${targetDir} (${err.message})`));
    }

    // 9. 打印开始备份的信息
    console.log(`📦 开始备份项目: ${projectRoot}`);
    console.log(`💾 目标路径: ${targetDir}`);
    console.log(`🚫 排除项: ${finalExcludes.join(', ')}`);

    // --- 执行备份阶段 ---

    // 10. 准备 robocopy 命令的参数
    // robocopy 是 Windows 系统自带的强大文件复制工具，支持断点续传、过滤、重试等功能
    const robocopyArgs = [
      projectRoot,              // 源目录：当前项目根目录
      targetDir,                // 目标目录：我们刚刚创建的备份目录
      '/E',                     // /E 表示复制所有子目录，包括空目录
      '/XD', ...finalExcludes,  // /XD 后跟要排除的目录列表
      '/XF', '*.log', '*.tmp',  // /XF 后跟要排除的文件模式
      '/R:1',                   // /R:1 表示如果复制失败，重试1次
      '/W:5',                   // /W:5 表示每次重试间隔5秒
      '/NP',                    // /NP 表示不在控制台显示文件复制的百分比进度（减少日志）
      '/NFL',                   // /NFL 表示不记录文件列表（减少日志）
      '/NDL',                   // /NDL 表示不记录目录列表（减少日志）
      // 注意：robocopy 的退出码含义特殊，0-7 通常表示成功（有警告），8+ 才是错误
    ];

    // 11. 启动 robocopy 子进程
    const robocopy = spawn('robocopy', robocopyArgs, { stdio: 'pipe' });

    // 12. 用于收集 robocopy 的输出日志（可选，用于调试）
    let output = '';

    // 13. 监听 robocopy 的标准输出（stdout）
    robocopy.stdout.on('data', (data) => {
      const chunk = data.toString();
      output += chunk;
      // 如果需要看到详细复制过程，可以取消下面这行的注释
      // console.log('[robocopy] ', chunk.trim());
    });

    // 14. 监听 robocopy 的错误输出（stderr）
    robocopy.stderr.on('data', (data) => {
      // stderr 通常包含警告信息，不是致命错误
      console.warn('[robocopy 警告] ', data.toString().trim());
    });

    // 15. 监听 robocopy 子进程启动失败的错误
    robocopy.on('error', (err) => {
      reject(new Error(`启动 robocopy 失败: ${err.message}`));
    });

    // 16. 监听 robocopy 子进程执行结束
    robocopy.on('close', (code) => {
      // robocopy 的退出码含义：
      // - 0: 没有复制任何文件（源和目标相同）
      // - 1: 成功复制了文件
      // - 2: 额外文件或目录在目标中被发现（不影响）
      // - 3: 文件或目录名称无效
      // - 8: 某些文件或目录无法访问
      // - 16: 严重错误（如目标不可写）
      // 因此，我们判断 code <= 7 为成功（即使有警告）
      if (code <= 7) {
        console.log(`✅ 项目成功备份到: ${targetDir}`);
        resolve({
          success: true,
          source: projectRoot,
          target: targetDir,
          timestamp,
          message: '项目备份成功',
          exitCode: code // 可以查看具体退出码了解情况
        });
      } else {
        // 如果 code >= 8，则认为是失败
        reject(new Error(`robocopy 执行失败，退出码: ${code}\n${output}`));
      }
    });
  });
};


module.exports = { backupDatabase, cleanupOldBackups,backupProjectToDrive };